package com.googlecode.kipler.container.generic;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import com.googlecode.kipler.common.Copyable;

/**
 * An implementation of {@link MultivalueMap} which provides a predictable
 * iteration order for its values. By predictable iteration order,
 * insertion-order should be understood.
 * 
 * @author İnanç Seylan
 */
public class MultivalueLinkedHashMap<K, V> implements MultivalueMap<K, V>,
		Copyable<MultivalueLinkedHashMap<K, V>> {

	private class KeyValuePair {
		private K key;

		private V value;

		public KeyValuePair(K key, V value) {
			setKey(key);
			setValue(value);
		}

		public K getKey() {
			return key;
		}

		public void setKey(K key) {
			this.key = key;
		}

		public V getValue() {
			return value;
		}

		public void setValue(V value) {
			this.value = value;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj != null && (obj.getClass().equals(this.getClass()))) {
				KeyValuePair other = (KeyValuePair) obj;
				return getKey().equals(other.getKey())
						&& getValue().equals(other.getValue());
			}
			return false;
		}

		@Override
		public int hashCode() {
			return 17 * 37 + getKey().hashCode() + getValue().hashCode();
		}

	}

	private Map<K, Set<V>> data = new LinkedHashMap<K, Set<V>>();

	private Set<KeyValuePair> pairs = new LinkedHashSet<KeyValuePair>();

	public void put(K key, V value) {
		if (containsKey(key)) {
			data.get(key).add(value);
		} else {
			Set<V> newSet = new LinkedHashSet<V>();
			newSet.add(value);
			data.put(key, newSet);
		}
		pairs.add(new KeyValuePair(key, value));
	}

	public boolean containsKey(K key) {
		return data.containsKey(key);
	}

	public boolean containsValue(V value) {
		boolean found = false;
		for (K key : data.keySet()) {
			if (data.get(key).contains(value)) {
				found = true;
				break;
			}
		}
		return found;
	}

	public boolean containsKeyValue(K key, V value) {
		return containsKey(key) && data.get(key).contains(value);
	}

	public Set<K> keySet() {
		return data.keySet();
	}

	public void remove(K key) {
		Set<V> values = getValuesForKey(key);
		for (V value : values) {
			pairs.remove(new KeyValuePair(key, value));
		}
		data.remove(key);
	}

	public void remove(K key, V value) {
		if (containsKey(key)) {
			Set<V> values = data.get(key);
			values.remove(value);
			pairs.remove(new KeyValuePair(key, value));
			if (values.isEmpty()) {
				data.remove(key);
			}
		}
	}

	public Set<V> getValuesForKey(K key) {
		Set<V> result;
		if (containsKey(key)) {
			result = data.get(key);
		} else {
			result = new LinkedHashSet<V>();
		}
		return result;
	}

	public Set<V> values() {
		Set<V> result = new LinkedHashSet<V>();
		for (KeyValuePair pair : pairs) {
			result.add(pair.getValue());
		}
		return result;
	}

	public MultivalueLinkedHashMap<K, V> copy() {
		MultivalueLinkedHashMap<K, V> theCopy = new MultivalueLinkedHashMap<K, V>();
		for (KeyValuePair pair : pairs) {
			theCopy.put(pair.getKey(), pair.getValue());
		}
		return theCopy;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj != null && (obj.getClass().equals(this.getClass()))) {
			MultivalueLinkedHashMap<?, ?> other = (MultivalueLinkedHashMap<?, ?>) obj;
			return data.equals(other.data);
		}
		return false;
	}

	@Override
	public int hashCode() {
		return data.hashCode();
	}

	@Override
	public int keySize() {
		return data.size();
	}

	@Override
	public int valueSize() {
		return pairs.size();
	}

}
